import { NextRequest, NextResponse } from 'next/server' import { createServerSupabaseClient } from '@/lib/supabase' // GET /api/books/[id]/github-integration - Check if GitHub integration exists export async function GET( request: NextRequest, { params }: { params: { id: string } } ) { try { const supabase = createServerSupabaseClient() const bookId = params.id // Get current user session (this works for client-side requests) const authHeader = request.headers.get('authorization') let user try { if (authHeader) { // Handle if there's an auth header const { data: { user: authUser }, error: authError } = await supabase.auth.getUser(authHeader.replace('Bearer ', '')) if (!authError) user = authUser } else { // Try to get user from session const { data: { user: sessionUser }, error: sessionError } = await supabase.auth.getUser() if (!sessionError) user = sessionUser } } catch (e) { // Ignore auth errors for now } if (!user) { return NextResponse.json( { error: 'Authentication required' }, { status: 401 } ) } // Verify user owns this book const { data: book, error: bookError } = await supabase .from('books') .select('id, user_id') .eq('id', bookId) .eq('user_id', user.id) .single() if (bookError || !book) { return NextResponse.json( { error: 'Book not found or access denied' }, { status: 404 } ) } // Check for GitHub integration in user metadata const { data: profile } = await supabase .from('profiles') .select('github_integrations') .eq('id', user.id) .single() const githubIntegrations = profile?.github_integrations || {} const integration = githubIntegrations[bookId] if (integration) { return NextResponse.json({ hasIntegration: true, repository_name: integration.repository_name, repository_full_name: integration.repository_full_name, repository_url: integration.repository_url, github_username: integration.github_username, is_private: integration.is_private, connected_at: integration.connected_at }) } else { return NextResponse.json({ hasIntegration: false }) } } catch (error) { console.error('Error checking GitHub integration:', error) return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ) } } // POST /api/books/[id]/github-integration - Set up GitHub integration via OAuth export async function POST( request: NextRequest, { params }: { params: { id: string } } ) { try { const supabase = createServerSupabaseClient() const bookId = params.id const { integration } = await request.json() if (!integration) { return NextResponse.json( { error: 'Integration data is required' }, { status: 400 } ) } // Get user ID from integration data const userId = integration.user_id if (!userId) { return NextResponse.json( { error: 'User ID required' }, { status: 400 } ) } // Verify user owns this book const { data: book, error: bookError } = await supabase .from('books') .select('id, user_id, title') .eq('id', bookId) .eq('user_id', userId) .single() if (bookError || !book) { return NextResponse.json( { error: 'Book not found or access denied' }, { status: 404 } ) } // Verify GitHub repo access try { const repoResponse = await fetch(`https://api.github.com/repos/${integration.repository_full_name}`, { headers: { 'Authorization': `Bearer ${integration.access_token}`, 'Accept': 'application/vnd.github.v3+json' } }) if (!repoResponse.ok) { return NextResponse.json( { error: 'Cannot access GitHub repository. Please check permissions.' }, { status: 400 } ) } } catch { return NextResponse.json( { error: 'Failed to verify GitHub repository access' }, { status: 400 } ) } // Get current profile const { data: profile } = await supabase .from('profiles') .select('github_integrations') .eq('id', userId) .single() const githubIntegrations = profile?.github_integrations || {} // Add this book's integration githubIntegrations[bookId] = { provider: integration.provider, access_token: integration.access_token, repository_url: integration.repository_url, repository_name: integration.repository_name, repository_full_name: integration.repository_full_name, github_username: integration.github_username, repository_id: integration.repository_id, is_private: integration.is_private, connected_at: new Date().toISOString() } // Update profile with GitHub integration const { error: updateError } = await supabase .from('profiles') .update({ github_integrations: githubIntegrations, updated_at: new Date().toISOString() }) .eq('id', userId) if (updateError) { console.error('Error updating profile:', updateError) return NextResponse.json( { error: 'Failed to save GitHub integration' }, { status: 500 } ) } // Make initial commit with all book files try { await makeInitialCommit(supabase, bookId, userId, integration) } catch (commitError) { console.error('Failed to make initial commit:', commitError) // Don't fail the integration setup if initial commit fails } return NextResponse.json({ message: 'GitHub integration set up successfully', repository_name: integration.repository_name, repository_full_name: integration.repository_full_name, repository_url: integration.repository_url, github_username: integration.github_username, is_private: integration.is_private }) } catch (error) { console.error('Error setting up GitHub integration:', error) return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ) } } // DELETE /api/books/[id]/github-integration - Remove GitHub integration export async function DELETE( request: NextRequest, { params }: { params: { id: string } } ) { try { const supabase = createServerSupabaseClient() const bookId = params.id // Get current user const { data: { user }, error: authError } = await supabase.auth.getUser() if (authError || !user) { return NextResponse.json( { error: 'Authentication required' }, { status: 401 } ) } // Verify user owns this book const { data: book, error: bookError } = await supabase .from('books') .select('id, user_id') .eq('id', bookId) .eq('user_id', user.id) .single() if (bookError || !book) { return NextResponse.json( { error: 'Book not found or access denied' }, { status: 404 } ) } // Get current profile const { data: profile } = await supabase .from('profiles') .select('github_integrations') .eq('id', user.id) .single() const githubIntegrations = profile?.github_integrations || {} // Remove this book's integration delete githubIntegrations[bookId] // Update profile const { error: updateError } = await supabase .from('profiles') .update({ github_integrations: githubIntegrations, updated_at: new Date().toISOString() }) .eq('id', user.id) if (updateError) { console.error('Error updating profile:', updateError) return NextResponse.json( { error: 'Failed to remove GitHub integration' }, { status: 500 } ) } return NextResponse.json({ message: 'GitHub integration removed successfully' }) } catch (error) { console.error('Error removing GitHub integration:', error) return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ) } } // Helper function to make initial commit with all book files async function makeInitialCommit(supabase: any, bookId: string, userId: string, integration: any) { // Get all book files const { data: files } = await supabase .from('file_system_items') .select('*') .eq('book_id', bookId) if (!files || files.length === 0) { throw new Error('No files found to commit') } // Build file structure const buildFilePath = (fileId: string, allFiles: any[]): string => { const file = allFiles.find(f => f.id === fileId) if (!file) return '' // For files, ensure extension is included let fileName = file.name if (file.type === 'file' && file.file_extension && !fileName.includes('.')) { fileName = `${fileName}.${file.file_extension}` } if (!file.parent_id) return fileName const parentPath = buildFilePath(file.parent_id, allFiles) return parentPath ? `${parentPath}/${fileName}` : fileName } // Create file contents for commit const fileContents: { [path: string]: string } = {} files.forEach((file: any) => { if (file.type === 'file') { const filePath = buildFilePath(file.id, files) fileContents[filePath] = file.content || '' } }) if (Object.keys(fileContents).length === 0) { throw new Error('No file content to commit') } // GitHub API details const owner = integration.github_username const repo = integration.repository_name const accessToken = integration.access_token console.log(`Making initial commit for ${owner}/${repo}`) // Add a small delay to allow GitHub to fully initialize the repository await new Promise(resolve => setTimeout(resolve, 1000)) // Try to get current branch reference (might not exist for empty repo) const branchResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/main`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) let parentSha = null let isEmptyRepo = false if (branchResponse.ok) { const branchData = await branchResponse.json() parentSha = branchData.object.sha console.log(`Found existing branch with SHA: ${parentSha}`) } else if (branchResponse.status === 404 || branchResponse.status === 409) { // Empty repository - no main branch exists yet (404) or repository is empty (409) isEmptyRepo = true console.log(`Empty repository detected (${branchResponse.status}) - will create initial commit`) } else { // Log the actual error for debugging const errorText = await branchResponse.text() console.error(`GitHub API error (${branchResponse.status}):`, errorText) throw new Error(`Failed to get branch reference: ${branchResponse.status} - ${errorText}`) } // For empty repositories, create tree directly without creating blobs first if (isEmptyRepo) { console.log('Creating tree directly for empty repository') // For truly empty repositories, we need to initialize with a dummy commit first // then we can use the proper Git Tree API console.log('Initializing empty repository with dummy commit') try { // Create a simple initial commit to initialize the repository const initResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/README.md`, { method: 'PUT', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Initialize repository', content: Buffer.from('# Repository initialized by BookWiz\n\nThis repository will contain your book files.').toString('base64') }) }) if (!initResponse.ok) { const errorText = await initResponse.text() console.error(`Failed to initialize repository (${initResponse.status}):`, errorText) throw new Error(`Failed to initialize repository: ${initResponse.status}`) } const initResult = await initResponse.json() const initCommitSha = initResult.commit.sha console.log(`Repository initialized with commit: ${initCommitSha}`) // Now we can use the regular Git Tree API with this as the base parentSha = initCommitSha isEmptyRepo = false // Treat it as a normal repo now // Continue to the normal blob creation process below } catch (initError) { console.error('Failed to initialize repository, falling back to Contents API:', initError) // Ultimate fallback - but let's make it create a single commit by combining files console.log('Using combined Contents API approach for empty repository') // Create all files at once using multiple concurrent requests but with the same commit message // This is still not ideal but better than sequential commits const fileEntries = Object.entries(fileContents) if (fileEntries.length === 1) { // If only one file, create it directly const [path, content] = fileEntries[0] const fileResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Initial commit: Add book files from BookWiz', content: Buffer.from(content).toString('base64') }) }) if (!fileResponse.ok) { const errorText = await fileResponse.text() throw new Error(`Failed to create file ${path}: ${fileResponse.status}`) } const result = await fileResponse.json() console.log('Created single file using Contents API') return result.commit } else { // Multiple files - this will unfortunately create multiple commits // but it's the only option for truly empty repos when Git Tree API fails console.log('WARNING: Will create multiple commits due to GitHub API limitations with empty repositories') let lastCommit = null for (const [path, content] of fileEntries) { console.log(`Creating file: ${path}`) const fileResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Initial commit: Add book files from BookWiz', content: Buffer.from(content).toString('base64') }) }) if (!fileResponse.ok) { const errorText = await fileResponse.text() console.error(`Failed to create file ${path} (${fileResponse.status}):`, errorText) throw new Error(`Failed to create file ${path}: ${fileResponse.status}`) } const result = await fileResponse.json() lastCommit = result.commit } console.log(`Created ${fileEntries.length} files using Contents API fallback`) return lastCommit } } } // Create blobs and tree entries const treeEntries = await Promise.all( Object.entries(fileContents).map(async ([path, content]) => { const blobResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/blobs`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify({ content: Buffer.from(content).toString('base64'), encoding: 'base64' }) }) if (!blobResponse.ok) { const errorText = await blobResponse.text() console.error(`Failed to create blob for ${path} (${blobResponse.status}):`, errorText) throw new Error(`Failed to create blob for ${path}: ${blobResponse.status}`) } const blob = await blobResponse.json() return { path, mode: '100644', type: 'blob', sha: blob.sha } }) ) // Create new tree const treePayload: any = { tree: treeEntries } // Set base_tree if we have a parent commit if (parentSha) { treePayload.base_tree = parentSha } const newTreeResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify(treePayload) }) if (!newTreeResponse.ok) { const errorText = await newTreeResponse.text() console.error(`Failed to create tree (${newTreeResponse.status}):`, errorText) throw new Error(`Failed to create tree: ${newTreeResponse.status}`) } const newTree = await newTreeResponse.json() // Create commit const commitPayload: any = { message: 'Initial commit: Add book files from BookWiz', tree: newTree.sha } // Set parents if we have a parent commit if (parentSha) { commitPayload.parents = [parentSha] } const commitResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify(commitPayload) }) if (!commitResponse.ok) { const errorText = await commitResponse.text() console.error(`Failed to create commit (${commitResponse.status}):`, errorText) throw new Error(`Failed to create commit: ${commitResponse.status}`) } const commit = await commitResponse.json() // Update branch reference (always update since we now have a parent commit) const updateRefResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/main`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify({ sha: commit.sha }) }) if (!updateRefResponse.ok) { const errorText = await updateRefResponse.text() console.error(`Failed to update branch reference (${updateRefResponse.status}):`, errorText) throw new Error(`Failed to update branch reference: ${updateRefResponse.status}`) } return commit }